<script>
  var countSpecialNumbers = function (n) {

    let s = String(n), dp = Array.from({ length: s.length }, () => Array(1 << 10).fill(-1))

    /**
     * @param {number} i 从第i开始填数字
     * @param {number} mask 用位数据存储已填数字的集合
     * @param {boolean} isLimit 表示前面的数字是否已和n对应, 决定本次的上限是 s[i] 还是 9 
     * @param {boolean} isNum 表示前面是否已填; 如果是，就代表当前不会是前导0了, 后续可以从0开始; 
     *                           如果不是，那么可以跳过; 主要调节初始化和后续填值的不同
     * @return {number} 返回从i开始填的数字结果;  记忆化结果dp[i][mask] 如果已经不是-1 代表已经计算过 
     */
    const f = (i, mask, isLimit, isNum) => {
      // 这次已经终点, 如果填过, 次数+1
      if (i === s.length) return isNum ? 1 : 0

      // 如果不受限且前面已填且有记忆结果  直接返回
      if (!isLimit && isNum && dp[i][mask] >= 0) return dp[i][mask]
      // 没有缓存 从0开始计算本次
      let res = 0
      // 如果前面跳过没填 那么本次也跳过; 因为这样不可能达到上界,isLimit变为false
      if (!isNum) res = f(i + 1, mask, false, false)
      // d 是尝试要在当前位放置的数字, up代表数字的上界是多少
      for (let d = isNum ? 0 : 1, up = isLimit ? +s[i] : 9; d <= up; ++d) {
        // 如果mask记录里 没有这个数字  那就往下算 并计入结果; 
        // 下次的isLimit的前提是 本次已经被isLimit 且达到上限
        if ((mask >> d & 1) === 0) res += f(i + 1, mask | (1 << d), isLimit && d === up, true)
      }
      // 如果自由不受限 且本次已填过 计入cache
      if (!isLimit && isNum) dp[i][mask] = res

      return res
    }
    // 从第0位开始, mask也是空, 受限, 前面没填过 
    return f(0, 0, true, false)
  };
  console.log(countSpecialNumbers(n = 20))
</script>